﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MethodWorx.CMS.Content;
using MethodWorx.CMS.Providers;
using MethodWorx.CMS.TemplateLanguage;
using MethodWorx.CMS.Plugins;
using MethodWorx.CMS.TemplateLanguage.Properties;
using System.Reflection;
using MethodWorx.CMS.TemplateLanguage.Design;
using MethodWorx.CMS.TemplateParser.Compiler;
using MethodWorx.CMS.Xml;

namespace MethodWorx.CMS.TemplateParser
{
    public class ParsingEngine
    {
        private TemplateHierarchy hierarchy;
        private TemplateFlattener flattener;
        private PluginFactory plugins;
        private Dictionary<string, object> variables;
        private Dictionary<IParser, TemplateElement> parsers = new Dictionary<IParser, TemplateElement>();
        private Dictionary<IParser, TemplateElement> parsedElements = new Dictionary<IParser, TemplateElement>();
        private bool parsed = false;
        private string placeholderName;
        private ICmsDataProvider provider;

        public ParsingEngine(ICmsDataProvider provider, PageTemplate leafTemplate, PluginFactory plugins, ITemporaryTemplateStorage storage)
            : this(provider, leafTemplate, plugins, storage, null)
        {
        }
        
        public ParsingEngine(ICmsDataProvider provider, PageTemplate leafTemplate, PluginFactory plugins, ITemporaryTemplateStorage storage, string placeholderName)
        {
            if(leafTemplate == null)
                throw new ArgumentNullException("leafTemplate");

            this.plugins = plugins;
            this.hierarchy = new TemplateHierarchy(leafTemplate);
            this.placeholderName = placeholderName;
            this.provider = provider;

            //
            //  ok, we  have flattened the hierarchy out
            //  so we need to overrwrite the templates
            //  with the ones from storage
            if(storage != null)
            {
                foreach(var template in this.hierarchy)
                {
                    template.Template
                        = storage.GetTemplateFromStorage(template);
                }
            }


            var root = this.hierarchy.GetRootTemplate();
            

            //
            //  ok, lets flatten the hierarchy
            this.flattener = new TemplateFlattener(placeholderName, this.hierarchy.Select(h => h.Template).ToArray());
        }

        public XmlParser Parser
        {

            get { return this.flattener.Parser; }
        }

        public TemplateElement FindDesignModelElementById(int p, int i)
        {
            if (!this.parsed)
                this.GetTemplateForDesignMode();

            //  
            //  ok, find the tmeplate element
            return
                this.flattener.Parser.Document.FlattenNodes<TemplateElement>().FirstOrDefault(
                    e => e.HasAttribute("_pi", p + ":" + i, true));
        }

        public string GetTemplateForDesignMode()
        {
            XmlParser parser = this.flattener.Parser;

            if (!this.parsed)
            {
                this.parsed = true;
                //  ok, here we go
                //
                //  before we do anything, we need to tag the nodes that can be edited on this document withsomething meaningful
                this.TagNodes(parser);
                if (parser.Document.GetErrors().Any())
                {
                    string ex = "Document contains errors: \r\n\r\n";
                    foreach (var error in parser.Document.GetErrors())
                    {
                        ex += error.Error + "\r\n";
                    }
                    throw new ParserException(ex);
                }

                //
                //  ok, so far so good :)
                //  we now need to find all the parsers and execute them
                this.ParseDesigners(parser);
            }

            return parser.Document.OuterXml;
        }

        private void TagNodes(XmlParser parser)
        {
            int p = 0;
            int i = 0;
            foreach(TemplateElement element in parser.Document.FlattenNodes<TemplateElement>())
            {
                element.AddAttribute("_pi", p + ":" + i);
                if (element.Name.Contains(":"))
                    p++;

                i++;
            }
        }

        public TemplateElement GetParserElement(int index)
        {
            if (!this.parsed)
                this.GetTemplateForDesignMode();

            return this.parsers.Values.Skip(index).Take(1).First();
        }

        public IParser GetParser(int index)
        {
            if (!this.parsed)
                this.GetTemplateForDesignMode();

            return this.parsers.Keys.Skip(index).Take(1).First();
        }

        private void ParseDesigners(XmlParser xml)
        {
            int index = 0;
            //  this contains all parsers that have been processed
            //  this ensures the while loop below does not get into an infinite loop
            List<TemplateElement> processed = new List<TemplateElement>();


            while (xml.Document.FlattenNodes<TemplateElement>().Where(e => e.Name.Contains(":") && !processed.Contains(e)).Any())
            {
                var node = xml.Document.FlattenNodes<TemplateElement>().Where(e => e.Name.Contains(":") && !processed.Contains(e)).First();
                if(node.HasNamespace)
                {
                    //
                    //  this is potentially a parser, so lets load it
                    var parser = this.GetParser(node, null);
                    if(parser != null)
                    {
                        if(parser is IDesignable)
                        {
                            parsers.Add(parser, node);
                            //  ok, are we designable
                            var context = new DesignerContext(this.provider);
                            ((IDesignable)parser).Design(context);

                            int removed = node.Remove();
                            if(context.ControlBuilder != null)
                            {
                                //
                                //  ok, what do we do if we have a control builder..
                                //  well, we take the pre content and the post content and create some more xml
                                if(!context.ControlBuilder.ChildControls.Any())
                                {
                                    //  we dont have any child controls, so, this is just straight forward rendering
                                    //  of all the divs with whatever containing html the designer has returned
                                    //  NB we can not the inner xml elements here because 
                                    XmlParser fromDesigner = new XmlParser(context.ControlBuilder.GetContentPreChildren() + context.ControlBuilder.GetContentPostChildren());
                                    //  so we create the from designer
                                    //  and now we insert the child
                                    var toReplace = fromDesigner.Document.FindElements("render:children", true).First();
                                    //  ok, take this one out
                                    var insertIndex = toReplace.Remove();
                                    //  and now add the children
                                    toReplace.Parent.InsertAt(node.Children.Where(c => c is TemplateElement || c is TemplateText), insertIndex);
                                     


                                    //  fromDesigner now contains the pre content, the child content and the post content
                                    //  we now enumerate all of the elements in this and add our CMS tags which enable the
                                    //  designer to interact via jquery)
                                    foreach(var designerNode in fromDesigner.Document.FlattenNodes<TemplateElement>())
                                    {
                                        designerNode.SetAttribute("cms-control", "true");
                                        designerNode.SetAttribute("cms-index", index.ToString());
                                        designerNode.SetAttribute("cms-name", context.ControlBuilder.Name);
                                    }
                                    //
                                    //  now we take these nodes and put them in place of the one we have removed
                                    node.Parent.InsertAt(fromDesigner.Document.Children.Where(c => c is TemplateElement || c is TemplateText), removed);
                                }
                                else
                                {
                                    //
                                    //  ok, we have controls, so this is a little bit more difficult
                                    // we have to do exactly the same above, execpt to copy the child content
                                    //  of each of the child elements that match the child control
                                    //  e.g if thewre is a child control called "default" we need to copy the children of <default> element
                                    List<TemplateElement> childContent = new List<TemplateElement>();
                                    foreach(var control in context.ControlBuilder.ChildControls)
                                    {
                                        var element = node.GetElement(control);
                                        if(element != null)
                                        {
                                            //  put the child content of this element into a parser
                                            //XmlParser fromDesigner = new XmlParser(element.InnerXml);
                                            //  ok, with this we have to add all the attributes to it
                                            foreach (var designerNode in element.FlattenNodes<TemplateElement>())
                                            {
                                                designerNode.AddAttribute("cms-childcontrol", control);
                                            }
                                            
                                            //
                                            //  ok, we append this now to the child content
                                            //childContent += fromDesigner.Document.OuterXml;
                                            childContent.Add(element);
                                            //  this gives us all our child controls
                                        }
                                    }
                                    //  this now contains the child content
                                    //  so its a case of wrapping it with the above
                                    XmlParser fromChildDesigner = new XmlParser(context.ControlBuilder.GetContentPreChildren() + context.ControlBuilder.GetContentPostChildren());

                                    //  so we create the from designer
                                    //  and now we insert the child
                                    var toReplace = fromChildDesigner.Document.FindElements("render:children", true).First();
                                    //  ok, take this one out
                                    var insertIndex = toReplace.Remove();
                                    //  and now add the children
                                    childContent.Reverse();
                                    foreach (var childElement in childContent)
                                    {
                                        toReplace.Parent.InsertAt(
                                            childElement.Children.Where(c => c is TemplateElement || c is TemplateText), insertIndex);
                                    }

                                    //  fromDesigner now contains the pre content, the child content and the post content
                                    //  we now enumerate all of the elements in this and add our CMS tags which enable the
                                    //  designer to interact via jquery, this adds the rest of the control attributes
                                    foreach (var designerNode in fromChildDesigner.Document.FlattenNodes<TemplateElement>())
                                    {
                                        designerNode.SetAttribute("cms-control", "true");
                                        //  and add the rest
                                        designerNode.SetAttribute("cms-index", index.ToString());
                                        designerNode.SetAttribute("cms-name", context.ControlBuilder.Name);
                                    }
                                    //
                                    //  now we take these nodes and put them in place of the one we have removed
                                    node.Parent.InsertAt(fromChildDesigner.Document.Children.Where(c => c is TemplateElement || c is TemplateText), removed);
                                }

                                parsedElements.Add(parser, node);
                            }
                            else
                            {
                                //  removed the contorl node for its children
                                node.Parent.InsertAt(node.Children.Where(n => !(n is TemplateAttribute)), removed);
                                parsedElements.Add(parser, node);
                            }
                            index++;
                        }
                    }
                }
                //  ok, the node has been dealt with
                processed.Add(node);
            }
        }

        public TemplateElement GetParsedElement(int index)
        {
            return this.parsedElements.Values.Skip(index).Take(1).FirstOrDefault();
        }

        private class DesignerContext : IDesigner
        {
            private ICmsDataProvider provider;

            public DesignerContext(ICmsDataProvider provider)
            {
                this.provider = provider;
            }

            private List<DesignerControlBuilder> builders = new List<DesignerControlBuilder>();

            public DesignerControlBuilder AddControl(string name)
            {
                if(builders.Count != 0)
                    throw new Exception("Only one control can be added per designable widget");

                builders.Add(new DesignerControlBuilder(name));
                return builders.Last();
            }

            public DesignerControlBuilder ControlBuilder
            {
                get { return this.builders.LastOrDefault(); }
            }


            public ICmsDataProvider GetProvider()
            {
                return this.provider;
            }
        }

        public IParser GetParser(string name, string nameSpace)
        {
             //
            //  ok, this is a special node that can be 
            //  parsed to an extension
            IParser parser = DefaultParserFactory.GetParsers().FirstOrDefault(
                p => p.Namespace.ToLower() == nameSpace.ToLower() &&
                     p.Name.ToLower() == name.ToLower());

            if (parser == null && this.plugins != null)
            {

                parser = this.plugins.GetTmlExtensions().FirstOrDefault(
                    p => p.Namespace.ToLower() ==nameSpace.ToLower() &&
                         p.Name.ToLower() == name.ToLower());
            }

            if(parser != null)
                return (IParser)Activator.CreateInstance(parser.GetType());
            else
            {
                return null;
            }
        }

        public IParser GetParser(TemplateElement node, Func<TemplateElement, IParser, Dictionary<string, object>> parameterResolver)
        {
            if(parameterResolver == null)
                parameterResolver =
                    ((n, p) => GetParametersFromParser(n, p) ?? new Dictionary<string, object>());

            var parser = this.GetParser(node.LocalName.ToLower(), node.Namespace.ToLower());

            if(parser != null)
            {
                //
                //  ok, get the parameters from the attributes
                Dictionary<string, object> parameters = parameterResolver(node, parser);
                //  get the value from the command
                this.SetParserProperties(parser, node);
            }

            return parser;
        }

        private Dictionary<string, object> GetParametersFromParser(TemplateElement node, IParser parser)
        {
            Dictionary<string, object> parameters = new Dictionary<string, object>();

            foreach (var attribute in node.Attributes)
            {
                if (!this.GetPropertyAttributes(parser).Where(p => attribute.Name.ToLower() == p.Alias.ToLower()).Any())
                {
                    attribute.Value = attribute.Value ?? "";
                    
                    if (attribute.Value.StartsWith("{") && attribute.Value.EndsWith("}"))
                    {
                        parameters.Add(attribute.Name.ToLower(), this.GetVariable(attribute.Value.TrimStart('{').TrimEnd('}')));
                    }
                    else
                    {
                        if (!parameters.ContainsKey(attribute.Name))
                            parameters.Add(attribute.Name.ToLower(), attribute.Value);
                        else
                            parameters[attribute.Name.ToLower()] = attribute.Value;
                    }
                }
            }

            return parameters;
        }

        private IEnumerable<PropertyAttribute> GetPropertyAttributes<T>(T parser)
           where T : IParser
        {
            foreach (var propertyInfo in typeof(T).GetProperties())
            {
                foreach (var attr in propertyInfo.GetCustomAttributes(true))
                {
                    if (attr is PropertyAttribute)
                    {
                        var pa = attr as PropertyAttribute;
                        if (string.IsNullOrEmpty(pa.Alias))
                            pa.Alias = propertyInfo.Name;

                        yield return (PropertyAttribute)attr;
                    }
                }
            }
        }

        public object GetVariable(string name)
        {
            return null;
        }
          private void SetParserProperties(IParser parser, TemplateElement node)
        {
            foreach (var attribute in node.Attributes)
            {
                foreach(var property in parser.GetType().GetProperties())
                {
                    bool set = false;
                    foreach (var attr in property.GetCustomAttributes(true))
                    {
                        if (attr is PropertyAttribute)
                        {
                            var pa = attr as PropertyAttribute;
                            if (string.IsNullOrEmpty(pa.Alias))
                                pa.Alias = property.Name;
                            if (pa.Alias.ToLower() == attribute.Name.ToLower())
                            {
                                this.SetPropertyValue(property, parser, attribute);
                                set = true;
                                break;
                            }
                        }
                    }
                    if(set)
                        break;
                }
            }
        }

        private void SetPropertyValue(PropertyInfo property, IParser parser, TemplateAttribute attribute)
        {
            var v = attribute.Value;
            if(v != null)
            {
                object converted;
                if (property.PropertyType == typeof(bool))
                {
                    switch (v.ToLower())
                    {
                        case "on":
                        case "true":
                        case "yes":
                        case "1":
                            converted = true;
                            break;
                        default:
                            converted = false;
                            break;
                    }
                }
                else
                {
                    if(property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        var type = property.PropertyType.GetGenericArguments()[0];
                        converted = Convert.ChangeType(v, type);
                    }
                    else
                        converted = Convert.ChangeType(v, property.PropertyType);
                }
                property.SetValue(parser, converted, null);
            }
        }

        public PageTemplate ReplaceElementWithParser(TemplateElement element, string parser, string initializer)
        {

            IParser parserInstance = null;
            try
            {
                parserInstance = this.GetParser(parser.Split(':')[1], parser.Split(':')[0]);
            }
            catch (Exception)
            {

            }
            foreach (var template in this.hierarchy)
            {
                if (template.Template == element.ParsedBuffer)
                {
                    string input = "";
                    var start = this.FindOpeningBracketFromPosition(template.Template,
                                                                       element.
                                                                           ElementNameStringPosition
                                                                           .Value);
                    var end = this.FindClosingBracketFromPosition(template.Template,
                                                                    element.ElementEndValueStringPosition.Value);
                    string templatePart;
                    templatePart = template.Template.Substring(start, (end - start) + 1);

                    if(parserInstance != null && parserInstance is IDesignerInitialization)
                    {
                        var xmlparser = new XmlParser(templatePart);
                        input = ((IDesignerInitialization)parserInstance).AddToTemplate((IXmlElement)(TemplateElement)xmlparser.Document.Children.First(), initializer);
                    }
                    else
                    {
                        switch((initializer ?? "").ToLower())
                        {
                            case "replace selection":
                                input = "<" + parser.TrimStart('<').TrimEnd('>').TrimEnd('/') + "/>";
                                break;
                            case "insert before":
                                input = "<" + parser.TrimStart('<').TrimEnd('>').TrimEnd('/') + "/>" + templatePart;
                                break;
                        }
                    }
                    //
                    //  ok, we didnt find the attribute
                    //  so we have to add it in
                    string templateXml =
                        template.Template.Substring(0,
                                                    this.FindOpeningBracketFromPosition(template.Template,
                                                                                        element.
                                                                                            ElementNameStringPosition
                                                                                            .Value))
                        + input  +
                        template.Template.Substring(
                            this.FindClosingBracketFromPosition(template.Template,
                                                                element.ElementEndValueStringPosition.Value) + 1);
                    template.Template = templateXml;
                    return template;
                }
            }
            return null;
        }



        public PageTemplate SetParserProperty(IParser parser, string name, string value)
        {
            var templateElement = this.parsers[parser];
            var templateXml = "";
            var length = 0;
            var position = 0;

            if (templateElement != null)
            {
                foreach (var template in this.hierarchy)
                {
                    if(template.Template == templateElement.ParsedBuffer)
                    {
                        //  this is the one we want
                        foreach(var attr in templateElement.Attributes)
                        {
                            if(attr.Name.ToLower() == name.ToLower())
                            {
                                if (!string.IsNullOrEmpty(value))
                                {
                                    length = attr.Value.Length;
                                    position = attr.AttributeValueStringPosition.Value;
                                    templateXml = template.Template.Substring(0, position) + value +
                                                  template.Template.Substring(position + length);

                                    template.Template = templateXml;
                                    attr.Value = value;
                                    return template;
                                }
                                else
                                {
                                    position = attr.AttributeNameStringPosition.Value;
                                    length = attr.Name.Length + attr.Value.Length + 4;
                                    templateXml = template.Template.Substring(0, position) + 
                                                 template.Template.Substring(position + length);
                                    template.Template = templateXml;
                                    return template;
                                }
                            }
                        }

                        if (!string.IsNullOrEmpty(value))
                        {
                            //
                            //  ok, we didnt find the attribute
                            //  so we have to add it in
                            templateXml =
                                template.Template.Substring(0,
                                                            templateElement.ElementNameStringPosition.Value +
                                                            templateElement.Name.Length) + " " + name + "=\"" + value +
                                "\" " +
                                template.Template.Substring(templateElement.ElementNameStringPosition.Value +
                                                            templateElement.Name.Length);
                            template.Template = templateXml;
                            return template;
                        }
                    }
                }
            }
            return null;
        }

        public PageTemplate InsertParser(IParser foundParser, string parser, string insertPosition)
        {
            var templateElement = this.parsers[foundParser];
            var templateXml = "";
            var length = 0;
            var position = 0;

            if (templateElement != null)
            {
                foreach (var template in this.hierarchy)
                {
                    if (template.Template == templateElement.ParsedBuffer)
                    {
                        int startingPos = -1;
                        switch(insertPosition.ToLower())
                        {
                            case "before":
                                startingPos = FindOpeningBracketFromPosition(template.Template,
                                                    templateElement.ElementNameStringPosition.Value);
                                break;
                            case "after":
                                startingPos = FindClosingBracketFromPosition(template.Template,
                                                    templateElement.ElementEndValueStringPosition.Value);
                                if (startingPos != -1)
                                    startingPos++;
                                break;
                        }
                        
                        if (startingPos != -1)
                        {
                            //
                            //  this is where we are inserting
                            templateXml =
                                template.Template.Substring(0, startingPos) +
                                "<" + parser + "/>" +
                                template.Template.Substring(startingPos);
                            template.Template = templateXml;
                            return template;
                        }
                    }
                }
            }
            return null;
        }

        public PageTemplate DeleteParser(IParser foundParser)
        {
            var templateElement = this.parsers[foundParser];
            return DeleteElement(templateElement);
        }

        public PageTemplate DeleteElement(TemplateElement templateElement)
        {
            var templateXml = "";
            var length = 0;
            var position = 0;

            if (templateElement != null)
            {
                foreach (var template in this.hierarchy)
                {
                    if (template.Template == templateElement.ParsedBuffer)
                    {
                        var startingPos = FindOpeningBracketFromPosition(template.Template, templateElement.ElementNameStringPosition.Value);
                        var endingPos = FindClosingBracketFromPosition(template.Template, templateElement.ElementEndValueStringPosition.Value);
                        if (startingPos != -1 && endingPos != -1)
                        {
                            //
                            //  this is where we are inserting
                            templateXml =
                                template.Template.Substring(0, startingPos) +
                                template.Template.Substring(endingPos + 1);
                            template.Template = templateXml;
                            return template;
                        }
                    }
                }
            }
            return null;
        }

        private int FindOpeningBracketFromPosition(string s, int pos)
        {
            while (pos >= 0)
            {
                if (s[pos] == '<')
                    return pos;

                pos--;
            }
            return -1;
        }

        private int FindClosingBracketFromPosition(string s, int pos)
        {
            while (pos < s.Length)
            {
                if (s[pos] == '>')
                    return pos;

                pos++;
            }
            return -1;
        }

        public TemplateCompiler GetCompiler()
        {
            return new TemplateCompiler(this);
        }



        public ICmsDataProvider GetProvider()
        {
            return this.provider;
        }
    }
}
